更新的部分實作上並沒有太困難的地方,主要是處理衝突比較麻煩。
更新的部分我是使用Handler和Runnable來處理,傳送的指令是"qrG"分別代表離開文章、閱讀文章、跳到文章頁尾,接著就是使用Day17的解析推文方法了。
private val updateHandler = Handler(Looper.getMainLooper())
private var isUpdating = false
private var isLoadingMore = false
private val updateRunnable = object : Runnable {
override fun run() {
if (isLoadingMore) return
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO)
{
isUpdating = true
PttClient.getInstance().send("qrG")
delay(200L)
commentList.clear()
parseComments(PttClient.getInstance().getScreen())
isUpdating = false
}
updateHandler.postDelayed(this, 2000)
}
}
在Runnable的結尾有再次呼叫updateHandler.postDelayed(this, 2000)
,如此持續更新推文,目前更新間隔是先設2秒(2000毫秒)。
而初次註冊updateRunnable的地方則是在Day17一進入PreviewFragment頁面時解析完推文後。
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
PttClient.getInstance().send("G")
delay(100L)
parseComments(PttClient.getInstance().getScreen())
updateHandler.postDelayed(updateRunnable, 2000)
}
最後為了讓更新後的RecyclerView持續在置底顯示最新推文,parseComments方法中有加入以下:
private suspend fun parseComments(screen: String) {
// ...
val currentPosition =
(binding.recyclerView.layoutManager as LinearLayoutManager)
.findLastVisibleItemPosition()
if (isUpdating && currentPosition == commentList.size - 1) {
withContext(Dispatchers.Main) {
binding.recyclerView.scrollToPosition(commentList.size - 1)
}
}
}
這項衝突主要是在當我滑動手機畫面來看上方舊推文時,我不希望因為自動更新又把我拉回置底畫面。
主要的解法就是上方parseComments程式碼區塊的currentPosition == commentList.size - 1
條件。
currentPosition的獲取是調用LinearLayoutManager的findLastVisibleItemPosition,如果當前RecyclerView的position不在最後一項我就會當作滑動中而停止置底畫面。
在Day19的內容中可以看到,在讀取更多推文時我是傳送了^B
指令,而更新推文需要傳送qrG
,這兩個指令要是一直交錯傳肯定是會有問題的,因此在讀取更多推文時我會將updateRunnable從updateHandler中移除,並且設置isLoadingMore的Flag來避免移除不及。
修改後的moreCommentCallback
adapter.moreCommentCallback = {
if (!isLoadingMore) {
val animator = ObjectAnimator.ofFloat(
binding.resumeUpdate,
"translationY",
200f.dpToPx(requireContext()).toFloat(),
0f
)
animator.interpolator = AccelerateDecelerateInterpolator()
animator.duration = 300
animator.start()
isLoadingMore = true
}
if (hasMore && !isUpdating) {
updateHandler.removeCallbacks(updateRunnable)
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
PttClient.getInstance().send(Char(2).toString())
delay(100L)
parseComments(PttClient.getInstance().getScreen())
}
}
}
若isUpdating為true
我也不會進行讀取更多推文的程式。此外可以看到我有用ObjectAnimator,這個是我在畫面中下加入的一個自動更新按鈕(待會畫面中能看到,就不放layout了)。按鈕的點擊事件就是用來恢復自動更新狀態的。
binding.resumeUpdate.setOnClickListener {
isLoadingMore = false
isUpdating = true
adapter.setData(listOf())
updateHandler.post(updateRunnable)
val animator = ObjectAnimator.ofFloat(
binding.resumeUpdate,
"translationY",
0f,
200f.dpToPx(requireContext()).toFloat()
)
animator.interpolator = AccelerateDecelerateInterpolator()
animator.duration = 300
animator.start()
}
調用updateHandler.post(updateRunnable)
來重新更新推文畫面,同時用adapter.setData(listOf())
將目前的內容清掉,避免恢復後沒有自動置底的狀態。
在離開PreviewFragment的過程中需把updateRunnable取消,否則也會有衝突導致畫面卡住或crash。
onBackPressedDispatcher
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
updateHandler.removeCallbacks(updateRunnable)
// ...
}
})
onDestroyView
override fun onDestroyView() {
updateHandler.removeCallbacks(updateRunnable)
super.onDestroyView()
// ...
}